/*
 * Copyright (C) 2009 M. Hautle.
 * 
 * This file is part of TrackedBean.
 * 
 * TrackedBean is free software: you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free
 * Software Foundation, either version 3 of the License, or (at your option) any later version.
 * 
 * TrackedBean is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
 * A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details.
 * 
 * You should have received a copy of the GNU Lesser General Public License along with TrackedBean. If not, see <http://www.gnu.org/licenses/>.
 */
package ch.trackedbean.copier.engine;

import java.beans.*;
import java.lang.reflect.*;
import java.util.*;
import ch.trackedbean.common.*;
import ch.trackedbean.copier.*;
import ch.trackedbean.util.*;

/**
 * Object holding a list mapping.<br>
 * Only source to destination mapping is supported.
 * 
 * @author M. Hautle
 */
public class ListMappingHolder extends AbstractSimpleMappingHolder {
	/** The type evaluator to use for {@link #copyToSource(Object, Object, IMappingContext)}. */
	protected ITypeEvaluator dstEvaluator;

	/**
	 * Default constructor.
	 * 
	 * @param src The source property
	 * @param dst The destination property (on the object holding the mapping annotations)
	 * @param evaluator The evaluator to use for the destination types
	 * @param lazy Flag indicating if the property should be fetched lazy
	 */
	public ListMappingHolder(PropertyDescriptor src, PropertyDescriptor dst, ITypeEvaluator evaluator, boolean lazy) {
		super(src, dst, false, lazy);
		if (evaluator == null)
			throw new IllegalArgumentException("The evaluator must be set!");
		dstEvaluator = evaluator;
	}

	/**
	 * Default constructor.
	 * 
	 * @param el The expression
	 * @param targetType The type of the value reurned by the el expression
	 * @param dst The destination property (on the object holding the mapping annotations)
	 * @param evaluator The evaluator to use for the destination types
	 * @param lazy Flag indicating if the property should be fetched lazy
	 */
	public ListMappingHolder(String el, Class targetType, PropertyDescriptor dst, ITypeEvaluator evaluator, boolean lazy) {
		super(el, targetType, dst, false, lazy);
		if (evaluator == null)
			throw new IllegalArgumentException("The evaluator must be set!");
		dstEvaluator = evaluator;
	}

	/**
	 * {@inheritDoc}
	 */
	@SuppressWarnings("unchecked")
	public void copyToDestination(Object src, Object dst, IMappingContext context) {
		try {
			final Object srcValue = srcProp.getValue(src, null);
			// set the destination to null if the source was null
			if (srcValue == null) {
				dstProp.setValue(dst, null);
				return;
			}
			dstProp.setValue(dst, convertToDst(srcValue, dstProp.getValue(dst, null), context));
		} catch (Exception e) {
			throw new TechnicalException("Error while copying " + srcProp.getPath() + " to " + dstProp.getPath(), e);
		}
	}

	/**
	 * Converts the given source into the corresponding destination.
	 * 
	 * @param srcValue The source value
	 * @param existingDst The existing destination value or null
	 * @param context The mapping context
	 * @return The destination value
	 */
	protected Object convertToDst(Object srcValue, Object existingDst, IMappingContext context) {
		final Collection src = convert(srcValue);
		final Class dstType = dstProp.getType();
		if (List.class == dstType)
			return convertToDst(src, new ArrayList(src.size()), context);
		if (Set.class == dstType)
			return convertToDst(src, new HashSet(), context);
		if (!dstType.isArray())
			throw new IllegalArgumentException("The destination type " + dstType.getName() + " is not supported!");
		// no existing array?
		if (existingDst == null)
			return convertToDst(src, (Object[]) Array.newInstance(dstType.getComponentType(), src.size()), 0, context);
		// allready existing array, we need to 'resize' it
		final Object[] old = (Object[]) existingDst;
		return convertToDst(src, BeanUtils.arrayExpand(old, src.size()), old.length, context);
	}

	/**
	 * 'Converts' the given source to a collection.
	 * 
	 * @param src The source
	 * @return The source as collection
	 */
	protected Collection convert(Object src) {
		if (src instanceof Collection)
			return (Collection) src;
		if (src instanceof Object[])
			return Arrays.asList((Object[]) src);
		throw new IllegalArgumentException("The source type " + src.getClass().getName() + " is not supported!");
	}

	/**
	 * Converts the given source into the corresponding destination collection.
	 * 
	 * @param src The source collection
	 * @param dst The destination collection
	 * @param context The mapping context
	 * @return The destination collection
	 */
	@SuppressWarnings("unchecked")
	protected Collection convertToDst(Collection src, Collection dst, IMappingContext context) {
		for (Object o : src)
			dst.add(convertToDstEntry(o, context));
		return dst;
	}

	/**
	 * Converts the given source into the corresponding destination array.
	 * 
	 * @param src The source collection
	 * @param dst The destination array
	 * @param i The index to use for the first entry
	 * @param context The mapping context
	 * @return The destination array
	 */
	private Object convertToDst(Collection src, Object[] dst, int i, IMappingContext context) {
		for (Object o : src)
			dst[i++] = convertToDstEntry(o, context);
		return dst;
	}

	/**
	 * Converts a source object to a destination object.
	 * 
	 * @param o The src object to convert
	 * @param context The mapping context
	 * @return The converted source object
	 */
	protected Object convertToDstEntry(Object o, IMappingContext context) {
		final Class<?> dstType = dstEvaluator.getCorrespondingType(o);
		// lookup allready converted objects
		final Object val = context.getElement(o, dstType);
		if (val != null)
			return val;
		// create and convert then (the mapper will register the instance in the context)
		final IBeanMapper m = BeanMapperManager.getMapper(dstType);
		return m.copySrc2Dst(o, create(dstType), context);
	}

	/**
	 * Creates an instance of the given type.
	 * 
	 * @param type The type to intanciate
	 * @return An instance fo the given type
	 */
	private Object create(final Class<?> type) {
		try {
			return type.newInstance();
		} catch (Exception e) {
			throw new TechnicalException("Error while trying to instanciate " + type.getName(), e);
		}
	}

	/**
	 * Does nothing.
	 * 
	 * @param dst not used
	 * @param src not used
	 * @param context not used
	 */
	public void copyToSource(Object src, Object dst, IMappingContext context) {
	}

	/**
	 * Does nothing.
	 * 
	 * @return null
	 */
	@Override
	public Object getDstValue(Object dst) {
		return null;
	}

	/**
	 * {@inheritDoc}
	 */
	@Override
	@SuppressWarnings("unchecked")
	public Object getSrcValue(Object src) {
		final Object srcValue = srcProp.getValue(src, null);
		if (srcValue == null)
			return null;
		return convertToDst(srcValue, null, NOPMappingContext.NOP);
	}
}